Appearance
在中大型业务系统中,代码最容易出现的问题不是“功能做不出来”,而是功能越做越多之后,代码越来越难维护。
常见情况包括:
- Controller 里写了一堆业务判断;
- Service 里既查数据库,又调用接口,还处理复杂业务规则;
- DAO、Mapper、缓存、消息队列代码和业务逻辑混在一起;
- 换一个数据库、改一个接口协议,就牵一发动全身;
- 单元测试很难写,因为业务逻辑强依赖数据库、Redis、Spring 容器。
领域驱动设计,也就是 DDD,强调围绕业务领域建模。而 DDD 分层架构的核心目标,就是把系统按照职责拆分成不同层次,让业务逻辑和技术实现解耦。
本文会结合一个实际项目模块划分,带你从零理解 DDD 分层架构。
一、什么是 DDD 分层架构?
DDD 分层架构是领域驱动设计中非常基础、也非常重要的一种架构模式。
它的核心思想是:
将系统按照职责划分为多个层次,每一层只关注自己的事情,层与层之间通过接口协作,保证领域逻辑不被技术细节污染。
简单来说,DDD 分层架构想解决的问题是:
- 业务逻辑应该放在哪里?
- 数据库操作应该放在哪里?
- HTTP 接口、MQ 消费者、定时任务应该放在哪里?
- Spring Boot 启动配置应该放在哪里?
- 公共枚举、异常、返回体应该放在哪里?
如果没有明确的分层,代码就很容易变成“大杂烩”。
二、为什么需要分层?
在没有清晰分层的项目中,代码通常会长这样:
java
@RestController
public class UserController {
@Autowired
private UserMapper userMapper;
@PostMapping("/register")
public Result register(@RequestBody RegisterRequest request) {
// 参数校验
// 查询数据库
// 判断用户是否存在
// 写入数据库
// 发送消息
// 返回结果
}
}表面上看,这样写很直接。
但是随着业务复杂度上升,会出现几个严重问题:
1. 业务逻辑和技术实现混在一起
Controller 本来应该只负责接收请求,但现在它还负责:
- 参数处理;
- 数据库查询;
- 业务判断;
- 消息发送;
- 返回封装。
这会导致一个类承担太多职责。
2. 代码难以测试
如果业务逻辑直接依赖数据库、Redis、MQ,那么测试业务规则时,就必须准备完整的外部环境。
比如你只是想测试“用户是否允许注册”这个业务规则,却不得不启动 Spring、连接数据库、准备测试数据。
3. 技术变更影响业务代码
如果项目原来使用 MyBatis,后来想换成 JPA,或者从 MySQL 改成其他存储方式,业务代码可能也要跟着改。
这说明业务逻辑已经被技术实现污染了。
4. 项目越大越难维护
小项目中,Controller-Service-Dao 的结构也许够用。
但当业务越来越复杂,Service 层往往会变成一个“万能层”:
- 写业务逻辑;
- 查数据库;
- 调第三方接口;
- 处理缓存;
- 发 MQ;
- 做事务控制。
最终 Service 层越来越臃肿,难以理解和维护。
三、DDD 分层架构的核心原则
DDD 分层架构最重要的原则是:
依赖方向由外向内,领域层处于核心位置。
也就是说:
- 外层可以依赖内层;
- 内层不能依赖外层;
- Domain 层是业务核心;
- Domain 层不应该依赖数据库、缓存、Web 框架、ORM 框架等技术细节。
可以简单理解为:
text
外部入口层 trigger
↓
领域核心层 domain
↑
基础设施层 infrastructure这里看起来有点奇怪:
- trigger 会调用 domain;
- infrastructure 也依赖 domain;
- 但 domain 不直接依赖 infrastructure。
这背后体现的就是依赖倒置原则。
四、项目模块划分示例
假设我们有一个实际项目,模块结构如下:
text
chatgpt-data
├── chatgpt-data-app
├── chatgpt-data-trigger
├── chatgpt-data-domain
├── chatgpt-data-infrastructure
└── chatgpt-data-types每个模块代表一层职责。
下面逐层讲解。
五、各层职责详解
1. 领域核心层:domain
对应模块:
text
chatgpt-data-domain这是整个系统的核心,也可以说是系统的“心脏”。
Domain 层主要存放业务相关的核心代码,包括:
- 实体;
- 值对象;
- 聚合根;
- 领域服务;
- 仓储接口;
- 业务规则;
- 领域行为。
Domain 层应该关注什么?
Domain 层只关心业务问题,例如:
- 用户能不能注册?
- 订单能不能支付?
- 优惠券是否可用?
- 账户余额是否足够?
- 某个业务状态能否流转到下一个状态?
它不应该关心:
- 数据是存在 MySQL 还是 MongoDB;
- 缓存用的是 Redis 还是本地缓存;
- 接口是 HTTP 还是 RPC;
- 框架是 Spring MVC 还是其他 Web 框架;
- ORM 是 MyBatis 还是 JPA。
Domain 层通常包含哪些内容?
一个典型的 domain 模块可能长这样:
text
chatgpt-data-domain
├── model
│ ├── entity
│ ├── valobj
│ └── aggregate
├── service
└── repository其中:
| 目录 | 说明 |
|---|---|
| entity | 实体,具有唯一标识和生命周期 |
| valobj | 值对象,描述某种业务概念,无唯一身份 |
| aggregate | 聚合根,负责维护一组对象的一致性 |
| service | 领域服务,处理不适合放在实体中的复杂业务逻辑 |
| repository | 仓储接口,只定义接口,不写具体实现 |
重点:Domain 层只定义 Repository 接口
例如:
java
public interface UserRepository {
User findByUserId(String userId);
void save(User user);
}这里的 UserRepository 只是一个接口。
Domain 层并不关心它最终是通过 MyBatis 查询数据库,还是通过 Redis、ES、远程接口获取数据。
这就保证了领域层的纯粹性。
2. 基础设施层:infrastructure
对应模块:
text
chatgpt-data-infrastructureInfrastructure 层负责所有技术实现。
它主要处理:
- 数据库访问;
- Redis 操作;
- MQ 发送;
- 第三方 API 调用;
- 文件存储;
- ORM 框架适配;
- Repository 接口实现。
如果说 domain 层负责“业务抽象”,那么 infrastructure 层负责“技术落地”。
Infrastructure 层通常包含哪些内容?
text
chatgpt-data-infrastructure
├── dao
├── mapper
├── repository
├── redis
├── gateway
└── config| 目录 | 说明 |
|---|---|
| dao / mapper | 数据库访问对象,例如 MyBatis Mapper |
| repository | 实现 domain 层定义的仓储接口 |
| redis | Redis 操作相关代码 |
| gateway | 第三方接口调用 |
| config | 基础设施相关配置 |
Repository 实现示例
Domain 层定义接口:
java
public interface UserRepository {
User findByUserId(String userId);
void save(User user);
}Infrastructure 层提供实现:
java
@Repository
public class UserRepositoryImpl implements UserRepository {
@Resource
private UserMapper userMapper;
@Override
public User findByUserId(String userId) {
UserPO userPO = userMapper.selectByUserId(userId);
return convertToDomain(userPO);
}
@Override
public void save(User user) {
UserPO userPO = convertToPO(user);
userMapper.insert(userPO);
}
}注意这里的关键点:
接口定义在 domain,具体实现放在 infrastructure。
这样做的好处是:
- Domain 层不依赖 MyBatis;
- Domain 层不知道数据库表结构;
- 更换存储方式时,只需要修改 infrastructure;
- 核心业务逻辑不会被技术细节影响。
3. 触发器层:trigger
对应模块:
text
chatgpt-data-triggerTrigger 层是系统对外暴露的入口。
常见入口包括:
- HTTP Controller;
- MQ Consumer;
- 定时任务 Job;
- RPC 接口;
- WebSocket 入口。
它的职责是接收外部请求,然后调用 domain 或 application 相关服务完成业务处理。
Trigger 层应该做什么?
Trigger 层适合处理:
- 接收请求;
- 参数转换;
- 参数基础校验;
- 调用业务服务;
- 返回结果封装。
例如:
java
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserDomainService userDomainService;
@PostMapping("/register")
public Result register(@RequestBody RegisterRequest request) {
userDomainService.register(request.getUserId(), request.getUserName());
return Result.success();
}
}Controller 不应该写复杂业务逻辑。
它更像是一个“适配器”,负责把外部请求转换成系统内部能够理解的调用。
Trigger 层不应该做什么?
Trigger 层不应该:
- 直接操作数据库;
- 编写复杂业务规则;
- 直接依赖 MyBatis Mapper;
- 处理大量状态流转逻辑;
- 把业务判断写在 Controller 里。
错误示例:
java
@PostMapping("/register")
public Result register(@RequestBody RegisterRequest request) {
UserPO userPO = userMapper.selectByUserId(request.getUserId());
if (userPO != null) {
return Result.fail("用户已存在");
}
userMapper.insert(...);
return Result.success();
}这种写法会让 Controller 变得越来越重,也会让业务规则分散在各个入口中。
4. 应用启动层:app
对应模块:
text
chatgpt-data-appApp 层是整个应用的启动入口。
它主要负责:
- Spring Boot 启动类;
- 配置文件;
- Bean 装配;
- 数据源配置;
- 中间件配置;
- 将各个模块组装起来运行。
例如:
text
chatgpt-data-app
├── Application.java
├── config
└── resources
├── application.yml
└── logback-spring.xmlApp 层本身一般不承载具体业务逻辑。
它更像是整个系统的“装配中心”。
5. 公共类型定义层:types
对应模块:
text
chatgpt-data-typesTypes 层用于存放跨模块复用的通用类型。
比如:
- 通用枚举;
- 常量定义;
- 公共异常;
- 通用返回体;
- 通用工具类;
- 基础 DTO;
- 错误码定义。
例如:
text
chatgpt-data-types
├── enums
├── constants
├── exception
├── response
└── utilsTypes 层可以被其他层共同依赖,目的是避免重复定义。
不过需要注意:
Types 层应该只放真正通用的内容,不要把业务逻辑塞进去。
如果所有东西都往 types 里放,types 很容易变成另一个“大杂烩”。
六、各层之间的依赖关系
DDD 分层架构中,依赖方向非常重要。
整体依赖关系可以理解为:
text
trigger → domain → types
infrastructure → domain → types
app → trigger
app → domain
app → infrastructure
app → types也就是说:
- trigger 依赖 domain;
- infrastructure 依赖 domain;
- domain 依赖 types;
- app 依赖所有模块,用于启动和组装;
- types 被各层共同依赖。
依赖方向示意图
mermaid
flowchart TD
Types[types 公共类型层]
Domain[domain 领域核心层]
Infrastructure[infrastructure 基础设施层]
Trigger[trigger 触发器层]
App[app 应用启动层]
Domain --> Types
Infrastructure --> Domain
Trigger --> Domain
App --> Trigger
App --> Domain
App --> Infrastructure
App --> Types这里需要重点理解:
Domain 层永远不应该反向依赖 infrastructure 或 trigger。
也就是说,下面这种依赖是错误的:
text
domain → infrastructure
domain → trigger因为一旦 domain 依赖了 infrastructure,就意味着业务逻辑开始依赖技术实现了。
七、代码依赖方向与运行时调用方向
DDD 分层中有一个新手很容易困惑的点:
为什么代码依赖方向和运行时调用方向好像是反的?
例如在 Repository 场景中:
代码依赖方向是:
text
infrastructure → domain → types但是运行时调用方向可能是:
text
trigger → domain → infrastructure看起来好像 domain 调用了 infrastructure。
实际上,domain 调用的是自己定义的接口,而不是 infrastructure 的具体实现。
依赖倒置的体现
假设 domain 层定义接口:
java
public interface UserRepository {
User findByUserId(String userId);
}infrastructure 层实现接口:
java
@Repository
public class UserRepositoryImpl implements UserRepository {
@Override
public User findByUserId(String userId) {
// 通过 MyBatis 查询数据库
}
}domain 层中的业务服务依赖接口:
java
public class UserDomainService {
private final UserRepository userRepository;
public UserDomainService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String userId) {
User user = userRepository.findByUserId(userId);
if (user != null) {
throw new BizException("用户已存在");
}
// 执行业务逻辑
}
}这里 UserDomainService 并不知道 UserRepository 的实现类是谁。
真正运行时,由 Spring 容器把 UserRepositoryImpl 注入进来。
这就是依赖倒置原则:
高层模块不依赖低层模块,两者都依赖抽象。
换句话说:
- Domain 层依赖 Repository 抽象;
- Infrastructure 层实现 Repository 抽象;
- Domain 层不关心具体技术实现;
- 技术细节被隔离在 Infrastructure 层。
八、DDD 分层架构和传统三层架构的区别
很多新手会问:
DDD 分层架构和传统 Controller-Service-Dao 三层架构有什么区别?
传统三层架构一般是:
text
Controller → Service → Dao这种结构简单清晰,适合很多普通项目。
但是问题在于,Service 层很容易变成“超级大杂烩”。
它既负责业务逻辑,又负责:
- 调用 Dao;
- 处理事务;
- 转换对象;
- 调用外部接口;
- 处理缓存;
- 编排流程。
DDD 分层架构则更强调:
业务逻辑属于 Domain 层,技术实现属于 Infrastructure 层,外部入口属于 Trigger 层。
对比来看:
| 维度 | 传统三层架构 | DDD 分层架构 |
|---|---|---|
| 核心层 | Service | Domain |
| 业务逻辑位置 | 通常在 Service | 明确放在 Domain |
| 数据库依赖 | Service 往往直接或间接依赖 DAO | Domain 只依赖 Repository 接口 |
| 技术隔离 | 较弱 | 较强 |
| 适合场景 | 简单 CRUD、中小型系统 | 复杂业务、中大型系统 |
| 可测试性 | 一般 | 更好 |
| 可维护性 | 项目变复杂后容易下降 | 更适合长期演进 |
九、代码到底应该放在哪一层?
这是学习 DDD 分层架构时最常见的问题。
可以按照下面的规则判断。
1. 如果是业务规则,放 domain
例如:
- 用户是否允许注册;
- 订单是否可以取消;
- 优惠券是否满足使用条件;
- 账户余额是否足够;
- 某个状态是否可以流转;
- 积分如何计算;
- 风控规则如何判断。
这些都属于业务核心,应该放在 domain 层。
2. 如果是技术实现,放 infrastructure
例如:
- SQL 查询;
- MyBatis Mapper;
- Redis 读写;
- MQ 发送;
- 第三方接口调用;
- 文件上传;
- 对象存储;
- Elasticsearch 查询。
这些都属于技术细节,应该放在 infrastructure 层。
3. 如果是接收外部请求,放 trigger
例如:
- Controller;
- MQ Consumer;
- 定时任务;
- RPC Provider;
- WebSocket Handler。
这些都是系统入口,应该放在 trigger 层。
4. 如果是应用启动和配置,放 app
例如:
- Spring Boot 启动类;
- application.yml;
- Bean 配置;
- 数据源配置;
- 扫描路径配置;
- 中间件配置。
这些都应该放在 app 层。
5. 如果是通用定义,放 types
例如:
- 通用返回对象;
- 通用异常;
- 错误码;
- 公共枚举;
- 常量;
- 工具类。
这些可以放在 types 层。
十、一个简单判断口诀
可以记住下面这个口诀:
业务规则放 domain,技术实现放 infrastructure,外部入口放 trigger,启动装配放 app,公共定义放 types。
再具体一点:
text
描述业务本身的代码 → domain
连接数据库、缓存、第三方系统的代码 → infrastructure
接收 HTTP、MQ、定时任务的代码 → trigger
启动类和配置装配 → app
通用枚举、异常、返回体 → types十一、分层会不会让代码量变多?
答案是:会。
DDD 分层架构初期确实会增加一些代码量,例如:
- Repository 接口;
- Repository 实现类;
- DTO 和领域对象转换;
- PO 和领域对象转换;
- 模块之间的调用关系;
- 一些额外的抽象。
对于非常简单的 CRUD 项目,这种分层可能显得有点“重”。
但是当项目规模变大后,分层带来的收益会非常明显。
分层带来的好处
1. 业务逻辑更清晰
核心业务都集中在 domain 层,找业务规则时不用到处翻 Controller、Mapper、Utils。
2. 技术实现更容易替换
如果数据库访问方式变了,只需要改 infrastructure 层。
Domain 层不用动。
3. 更容易单元测试
Domain 层不依赖数据库、Redis、MQ,可以直接通过 mock Repository 接口来测试业务逻辑。
4. 团队协作更清楚
不同层职责明确,团队成员更容易知道代码应该写在哪里。
5. 系统更适合长期演进
业务系统通常不是一次性写完的,而是长期迭代的。
DDD 分层架构可以降低后续需求变更带来的影响范围。
十二、常见误区
误区一:DDD 就是多建几个模块
不是。
DDD 分层架构不是简单地把代码拆成几个 Maven 模块。
真正重要的是:
每一层的职责边界是否清晰,依赖方向是否正确,领域逻辑是否独立。
如果只是把 Controller、Service、Mapper 换个目录名,但业务逻辑仍然和数据库操作混在一起,那并不是真正的 DDD 分层。
误区二:Domain 层可以直接调用 Mapper
不可以。
Mapper 是典型的基础设施实现,应该放在 infrastructure 层。
Domain 层如果直接调用 Mapper,就会依赖 MyBatis 这样的技术框架,破坏领域层的纯粹性。
误区三:所有逻辑都放 Domain 层
也不对。
Domain 层放的是业务核心逻辑。
但像下面这些内容不应该放在 domain:
- HTTP 参数解析;
- 数据库 SQL;
- Redis Key 拼接;
- MQ Topic 配置;
- 第三方接口请求组装;
- Spring Bean 配置。
这些都属于外部输入或技术细节。
误区四:Types 层什么都能放
Types 层只适合放通用定义。
不要把业务服务、数据库对象、复杂工具类都塞进去。
否则 types 层会变成一个新的垃圾桶模块。
十三、推荐的模块依赖结构
一个比较清晰的依赖结构可以是:
text
chatgpt-data-app
depends on:
chatgpt-data-trigger
chatgpt-data-domain
chatgpt-data-infrastructure
chatgpt-data-types
chatgpt-data-trigger
depends on:
chatgpt-data-domain
chatgpt-data-types
chatgpt-data-infrastructure
depends on:
chatgpt-data-domain
chatgpt-data-types
chatgpt-data-domain
depends on:
chatgpt-data-types
chatgpt-data-types
depends on:
none核心原则是:
text
domain 不能依赖 infrastructure
domain 不能依赖 trigger
domain 尽量保持纯粹十四、总结
DDD 分层架构的核心,不是为了让项目看起来更复杂,而是为了让系统在业务增长后依然保持清晰、稳定、可维护。
它的关键思想可以总结为:
Domain 层是核心
业务逻辑应该放在领域层,而不是散落在 Controller 或基础设施代码中。Infrastructure 层负责技术实现
数据库、缓存、第三方接口、MQ 等技术细节都应该隔离在基础设施层。Trigger 层负责外部入口
HTTP、MQ、定时任务等入口只负责接收请求和触发业务流程。App 层负责启动装配
它是系统的启动入口,负责把各层组装起来。Types 层负责通用定义
公共枚举、异常、常量、返回体等可以放在这里。依赖方向必须稳定
外层依赖内层,领域层不依赖技术实现。
一句话概括:
DDD 分层架构的本质,是让业务逻辑摆脱技术细节的束缚,成为系统真正稳定、可复用、可测试的核心。